package weather_service.weatherprovider;

import Task.ProgressMonitor.*;
import Task.Support.CoreSupport.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.lang.builder.*;
import org.apache.commons.logging.*;
import org.jdom.*;
import org.jdom.input.*;
import weather_service.weatherprovider.locationdatamodel.*;
import weather_service.weatherprovider.weatherdatamodel.*;

import java.io.*;
import java.util.*;

/**
 * Gateway to the weather.com XML Data Feed.  Performs its own caching,
 * in compliance with the mandatory refresh rates in the weather.com
 * implementation guide.
 */
public class WeatherGateway {
//:::::::::::::::::::::::::::::::::::::::::::::::
// constants
//:::::::::::::::::::::::::::::::::::::::::::::::
//"local" constants
protected static Log LOG = LogFactory.getLog(WeatherGateway.class);
private static final String GET_LOCAL_FORECAST_URI = "weather/local/";
private static final String GET_CURRENT_CONDITIONS = "cc";
private static final String GET_FORECAST = "dayf";

private static final String PRODUCT_CODE = "prod";
private static final String LINK_CODE = "link";
private static final String XOAP_CODE = "xoap";
private static final String PARTNER_ID = "par";
private static final String LICENSE_KEY = "key";
private static final String UNIT = "unit";  // s or m

// Mandatory refresh rates per the Weather.com Implementation Guide
private static final long LOCATION_TTL = 1000 * 60 * 60 * 12;
private static final long CURRENT_CONDITIONS_TTL = 1000 * 60 * 30;
private static final long FORECAST_TTL = 1000 * 60 * 60 * 2;

//"search" constants
private static final String GET_SEARCH_LOCATIONS_URI = "search/search";
private static final String SEARCH_FOR_LOCATIONS_WHERE = "where";

//:::::::::::::::::::::::::::::::::::::::::::::::
// data members
//:::::::::::::::::::::::::::::::::::::::::::::::
private String weatherServer;
private String partnerID;
private String licenseKey;

private NameValuePair userid;
private NameValuePair password;
private NameValuePair prod;
private NameValuePair link;

private boolean defaultMetricSystem = false;

private Map weatherCache = new HashMap();

private UIHookAdapter _hook = null;

//:::::::::::::::::::::::::::::::::::::::::::::::
// factory method
//:::::::::::::::::::::::::::::::::::::::::::::::
public static WeatherGateway getDefaultInstance() {
  return new WeatherGateway(new DefaultWeatherConfigurationBean());
}

//:::::::::::::::::::::::::::::::::::::::::::::::
// constructor
//:::::::::::::::::::::::::::::::::::::::::::::::
public WeatherGateway(WeatherConfigurationIF config) {
  licenseKey = config.getLicenseKey();
  partnerID = config.getPartnerID();
  weatherServer = config.getWeatherServer();
  defaultMetricSystem = config.isMetric();

  userid = new NameValuePair(PARTNER_ID, partnerID);
  password = new NameValuePair(LICENSE_KEY, licenseKey);
  prod = new NameValuePair(PRODUCT_CODE, XOAP_CODE);
  link = new NameValuePair(LINK_CODE, XOAP_CODE);
}

//XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// taskapi - progressmonitor support
//XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
public void setUIHook(UIHookAdapter hook) {
  _hook = hook;
}

//:::::::::::::::::::::::::::::::::::::::::::::::
// functions to access "search" service
//:::::::::::::::::::::::::::::::::::::::::::::::
public SearchLocations searchForLocations(String where) throws WeatherError, IOException {
  //build query string
  NameValuePair searchWhere = new NameValuePair(SEARCH_FOR_LOCATIONS_WHERE, where);

  SearchLocations locations = doSearchQuery(
      new NameValuePair[]{searchWhere});

  return locations;
}

//:::::::::::::::::::::::::::::::::::::::::::::::
// "search" service communications with server
//:::::::::::::::::::::::::::::::::::::::::::::::
protected SearchLocations doSearchQuery(NameValuePair[] queryString) throws WeatherError, IOException {
  HttpClient client = new HttpClient();

  HttpMethod method = new GetMethod(weatherServer + GET_SEARCH_LOCATIONS_URI);
  method.setQueryString(queryString);

  URI uri = method.getURI();
  LOG.info("Performing query with URI:  " + uri);

  //generate the return report object...
  SearchLocations locations = null;

  //actually get the response from the server
  client.executeMethod(method);
//      byte[] dataFromServer = method.getResponseBody();
//      method.releaseConnection();
  ByteBuffer dataFromServer = HttpUtils.getMonitoredResponse(_hook, (HttpMethodBase) method);

  //try and parse the response from the server
  try {
    SAXBuilder saxBuilder = new SAXBuilder();
    Document document = saxBuilder.build(dataFromServer.getInputStream());
    Weather_20_XMLParser parser = new Weather_20_XMLParser();
    locations = parser.parseSearchQuery(document);
  }
  catch (JDOMException e) {
    throw new WeatherError(WeatherError.ErrorTypes.SEARCH_LOCATIONS_PARSING_ERROR, e);
  }

  return locations;

}

//:::::::::::::::::::::::::::::::::::::::::::::::
// functions to access "local" service
//:::::::::::::::::::::::::::::::::::::::::::::::
public WeatherReport getFullForecast(String locationID, int forecastLength) throws WeatherError, IOException {
  //build query string
  NameValuePair currentConditions = new NameValuePair(GET_CURRENT_CONDITIONS, "*");
  NameValuePair forecast = new NameValuePair(GET_FORECAST, String.valueOf(forecastLength));
  NameValuePair useMetricSystem = new NameValuePair(UNIT, getUnitString(defaultMetricSystem));

  WeatherReport report = doWeatherQuery(
      locationID,
      CURRENT_CONDITIONS_TTL,
      new NameValuePair[]{userid, password, prod, link, currentConditions, forecast, useMetricSystem});

  return report;
}

public WeatherReport getLocation(String locationID) throws WeatherError, IOException {
  WeatherReport report = doWeatherQuery(locationID, LOCATION_TTL, new NameValuePair[]{userid, password, prod, link});

  return report;
}

public WeatherReport getCurrentConditions(String locationID) throws WeatherError, IOException {
  return getCurrentConditions(locationID, defaultMetricSystem);
}

public WeatherReport getCurrentConditions(String locationID, boolean metricSystem) throws IOException, WeatherError {
  NameValuePair currentConditions = new NameValuePair(GET_CURRENT_CONDITIONS, "*");
  NameValuePair useMetricSystem = new NameValuePair(UNIT, getUnitString(metricSystem));

  WeatherReport report =
      doWeatherQuery(locationID,
                     CURRENT_CONDITIONS_TTL,
                     new NameValuePair[]{userid, password, prod, link, currentConditions, useMetricSystem});

  return report;
}

public WeatherReport getForecast(String locationID, int forecastLength) throws IOException, WeatherError {
  return getForecast(locationID, forecastLength, defaultMetricSystem);
}

public WeatherReport getForecast(String locationID, int forecastLength, boolean metricSystem)
    throws IOException, WeatherError
{
  NameValuePair forecast = new NameValuePair(GET_FORECAST, String.valueOf(forecastLength));
  NameValuePair useMetricSystem = new NameValuePair(UNIT, getUnitString(metricSystem));

  WeatherReport report =
      doWeatherQuery(locationID, FORECAST_TTL, new NameValuePair[]{userid, password, prod, link, forecast, useMetricSystem});

  return report;
}

public void setDefaultMetricSystem(boolean defaultMetricSystem) {
  this.defaultMetricSystem = defaultMetricSystem;
}

//:::::::::::::::::::::::::::::::::::::::::::::::
// "local" service communications with server
//:::::::::::::::::::::::::::::::::::::::::::::::
/**
 * this method actually performs the "local" query against the weather.com server.
 * the response is an XML document with the <weather> element and data, or an <error> elment and errors.
 * It should not return an empty document - if it does, then an a WeatherError.WEATHER_PARSING_ERROR will be thrown.
 * If there is a problem getting this call over the network, then an IOException is thrown.
 */
protected WeatherReport doWeatherQuery(String locationID, long allowedAge, NameValuePair[] query)
    throws WeatherError, IOException
{
  HttpClient client = new HttpClient();

  HttpMethod method = new GetMethod(weatherServer + GET_LOCAL_FORECAST_URI + locationID);
  method.setQueryString(query);

  URI uri = method.getURI();
  LOG.info("Performing query with URI:  " + uri);

  //generate the return report object...
  WeatherReport report = null;

  if (weatherCache.containsKey(uri)) {
    WeatherCacheEntry entry = (WeatherCacheEntry) weatherCache.get(uri);

    long lastUpdated = entry.getLastUpdated().getTimeInMillis();
    LOG.debug("Cache entry last updated:  " + lastUpdated);
    long now = new GregorianCalendar().getTimeInMillis();
    LOG.debug("Cache entry current time:  " + now);
    long diffMillis = now - lastUpdated;
    LOG.debug("Cache entry time difference is:  " + diffMillis);

    if (diffMillis < allowedAge) {
      report = entry.getReport();
      LOG.debug("Returning channel from cache.");
      return report;
    }
  }

  //actually get the response from the server
  client.executeMethod(method);
//    byte[] dataFromServer = method.getResponseBody();
//    method.releaseConnection();
  ByteBuffer dataFromServer = HttpUtils.getMonitoredResponse(_hook, (HttpMethodBase) method);

  //try and parse the response from the server
  try {
    SAXBuilder saxBuilder = new SAXBuilder();
    Document document = saxBuilder.build(dataFromServer.getInputStream());
    Weather_20_XMLParser parser = new Weather_20_XMLParser();
    report = parser.parseWeatherQuery(document, false);
  }
  catch (JDOMException e) {
    throw new WeatherError(WeatherError.ErrorTypes.WEATHER_PARSING_ERROR, e);
  }

  if (report != null) {
    WeatherCacheEntry entry = new WeatherCacheEntry(report);
    weatherCache.put(uri, entry);
    LOG.debug("Returning channel from network.");
  }

  return report;
}

private String getUnitString(boolean metricSystem) {
  String unit = "s";
  if (metricSystem) {
    unit = "m";
  }
  else {
    unit = "s";
  }
  return unit;
}

//:::::::::::::::::::::::::::::::::::::::::::::::
// weather cache manipulation
//:::::::::::::::::::::::::::::::::::::::::::::::
private class WeatherCacheEntry implements Serializable {
  static final long serialVersionUID = 5675883260124394705L;

  private Calendar lastUpdated;
  private WeatherReport report;

  public WeatherCacheEntry(WeatherReport report) {
    Calendar now = new GregorianCalendar();
    this.lastUpdated = now;
    this.report = report;
  }

  public WeatherReport getReport() {
    return report;
  }

  public Calendar getLastUpdated() {
    return lastUpdated;
  }
}

public String toString() {
  return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}

}

